/**
*  Copyright (c) 2011, Alex Theodoridis
*  All rights reserved.

*  Redistribution and use in source and binary forms, with
*  or without modification, are permitted provided that the
*  following conditions are met:
*  Redistributions of source code must retain the above
*  copyright notice, this list of conditions and the following disclaimer.
*  Redistributions in binary form must reproduce the above
*  copyright notice, this list of conditions and the following
*  disclaimer in the documentation and/or other materials
*  provided with the distribution.

*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS
*  AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
*  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
*  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
*  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
*  OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
*  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
*  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
*  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
*  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
*  ANY WAY OUT OF THE USE OF THIS SOFTWARE,
*  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
*/

#ifndef BEPTrainerH
#define BEPTrainerH
#include <NeuralNetwork/Perceptron/Perceptron.h>
#include <NeuralNetwork/LearningAlgorithm/BackPropagation/BPNeuralLayer.h>
#include <NeuralNetwork/LearningAlgorithm/BackPropagation/ErrorFunction.h>
#include <queue>
#include <list>
#include <algorithm>
#include <iterator>
#include <array>
#include <functional>
#include <Utilities/Math/Math.h>
#include <Utilities/System/Time.h>
#include <limits>
#include <random>
#include <chrono>
#include <boost/numeric/conversion/cast.hpp>

namespace nn {

namespace bp {

template<typename PerceptronType,
         unsigned int inputsNumber,
         unsigned int outputsNumber,
         template <class> class ErrorCalculator = SquaredError
		>
class BepAlgorithm {
private:
    typedef typename PerceptronType::Var Var;

public:
    typedef typename std::tuple< std::array<Var,inputsNumber>, std::array<Var, outputsNumber> > Prototype;

private:
    typedef typename PerceptronType::NeuralLayer::NeuralLayer OriginalNeuralLayer;
    typedef typename OriginalNeuralLayer::template rebind< BPNeuron>::type BPSubLayer;
    typedef typename nn::Perceptron< BPNeuralLayer< BPSubLayer, Prototype  >, PerceptronType::template OutputActivationFunctionType > Perceptron;
    typedef typename Perceptron::NeuralLayer NeuralLayer;

private:
    /// @brief current perceptron.
    Perceptron m_perceptron;

    /// @brief error limit, algorithm will stop execution when we reach this error limit.
    float m_maxError;

    /// @brief the learning rate.
    Var m_leariningRate;

    /// @brief outputs stored for each step.
    std::vector<Var> m_outputs;

    /// @brief execution error calculator.
    ErrorCalculator<typename PerceptronType::Var> m_errorCalculator;

    struct DummyMomentum{
      Var operator()( const Var& oldDelta, const Var& newDelta ){
	return newDelta;
      }
    };
    
public:
    /// @brief constructor will initialize the object with a learning rate and maximum error limit.
    /// @param varP the learning rate.
    /// @param maxError the limit for the error. Algorithm will stop when we reach the limit.
    BepAlgorithm ( Var learningRate, float maxError) : 	m_perceptron(inputsNumber, outputsNumber)
        ,m_maxError ( maxError )
        ,m_leariningRate(learningRate)
        ,m_outputs(inputsNumber, boost::numeric_cast<Var>(0.f) )
        ,m_errorCalculator()
    {
    }

    /// @brief will add a new layer to the current network.
    /// @param neuronsNumber the number of neurons ins a new layer.
    void addLayer(unsigned int neuronsNumber) {
        m_perceptron.addLayer(neuronsNumber);
        m_outputs.resize( m_perceptron.getOutputLayer()->size() );
    }

    /// @brief execution of the single learning step in this algorithm.
    /// @param prototype a prototype used for this step.
    /// @param momentum a callback which will calculate a new delta, used in order to introduce momentum.
    /// @return error on this step.
    Var executeTrainingStep ( Prototype& prototype,  std::function<Var(const Var&, const Var&)> momentum) {
        //Calculate values with current inputs
        m_perceptron.calculateOutputs( std::get<0>(prototype).begin(),  std::get<0>(prototype).end(), m_outputs.begin() );

        //Calculate deltas
        m_perceptron.getOutputLayer()->calculateOutputDeltas(prototype, momentum);
        auto i = m_perceptron.rbegin();
        (*i)->calculateLayerDeltas( m_perceptron.getOutputLayer(), momentum );
        auto nextLayer = i;
        i++;
        while( i != m_perceptron.rend() )
        {
            (*i)->calculateLayerDeltas( *nextLayer, momentum );
            nextLayer = i;
            i++;
        }

        //Calculate weights
        using namespace std::placeholders;
        std::for_each (m_perceptron.begin(), m_perceptron.end(), [this](NeuralLayer& layer) {
            layer->calculateLayerWeights(m_leariningRate);
        } );

        m_perceptron.getOutputLayer()->calculateLayerWeights(m_leariningRate);
        m_perceptron.calculateOutputs( std::get<0>(prototype).begin(),  std::get<0>(prototype).end(), m_outputs.begin() );

        //Calculate error
        return m_errorCalculator(m_outputs.begin(), m_outputs.end(),std::get<1>(prototype).begin() );
    }

    /// @brief will calculate a perceptron with appropriate weights.
    /// @param begin iterator which points to the first input.
    /// @param end iterator which points to the last input.
    /// @param ReportFunc error report function (callback).
    /// @param MomentumFunc function which will calculate a momentum.
    /// @return a calculated perceptron.
    template<typename Iterator>
    PerceptronType calculatePerceptron ( Iterator begin, Iterator end,
                                         std::function<void(const Var&)> func,
										 unsigned int maxNumberOfEpochs = std::numeric_limits< unsigned int >::max(),
                                         std::function<Var(const Var&, const Var&)> momentum = DummyMomentum()
                                       ) {

        unsigned int prototypesNumber = std::distance(begin, end);
        Var error = boost::numeric_cast<Var>(0.f);
        unsigned int epochCounter = 0;
        typename std::vector<Prototype > prototypes(begin, end);

        do {
            error = 0;
            auto seed = std::chrono::system_clock::now().time_since_epoch().count();
            std::shuffle( prototypes.begin(), prototypes.end(), std::default_random_engine(seed) );

            while ( !prototypes.empty() ) {
                Prototype prototype = prototypes.back();
                prototypes.pop_back();
                error += executeTrainingStep ( prototype, momentum );
            }

            func (error);
 	    if( epochCounter < std::numeric_limits< unsigned int >::max() ){
	      epochCounter++;
	    } 
	    
            prototypes.insert( prototypes.begin(), begin, end );
        } while ( error > m_maxError
                  && (epochCounter < maxNumberOfEpochs
                      || maxNumberOfEpochs == std::numeric_limits< unsigned int >::max()
                     )
                );

        PerceptronType perceptron( inputsNumber,  outputsNumber );
        perceptron.setMemento( m_perceptron.getMemento() );

        return perceptron;
    }

    ~BepAlgorithm() {
    }
};
//---------------------------------------------------------------------------

}

}

#endif

